/** PettyXML - A very basic xml parser
  * Copyright (c) 2008 Philipp Gildein (rmbl@openspeak-project.org)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification,
  * are permitted provided that the following conditions are met:
  *
  *    * Redistributions of source code must retain the above copyright notice,
  *         this list of conditions and the following disclaimer.
  *    * Redistributions in binary form must reproduce the above copyright notice,
  *         this list of conditions and the following disclaimer in the documentation
  *         and/or other materials provided with the distribution.
  *
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
  * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */

#include "pettyxml.hpp"

/** \brief Check if the char is a whitespace character
 *  \param c The char to check
 *  \return True if the char is a whitespace character
 */
bool isWhiteSpace(char c)
{
    return (c == ' ' || c == '\r' || c == '\n' || c == '\t');
}

/** \brief Strip all trailing whitespace characters
 *  \param text The string to strip
 *  \return The stripped string
 */
std::string stripTrailingWhiteSpace(const std::string &text)
{
    if (text.empty())
        return text;
    std::string tmp = text;
    while (isWhiteSpace(tmp[0]))
        tmp = tmp.substr(1);
    return tmp;
}

/** \brief Strip the ' and " tags
 *  \param text The string to strip
 *  \return The stripped string
 */
std::string stripClosingTags(const std::string &text)
{
    if (text.empty())
        return text;
    std::string tmp = text;
    if (tmp[0] == '\'' || tmp[0] == '"')
        tmp = tmp.substr(1);
    std::string::size_type i = tmp.size()-1;
    if (tmp[i] == '\'' || tmp[i] == '"')
        tmp.erase(i);
    return tmp;
}

/** \brief Check if the char is an alphabetic character
 *  \param c The char to check
 *  \return True if the char is an alphabetic character
 */
bool isAlpha(const char &c)
{
    if (c < 127)
        return isalpha(c);
    else
        return true;
}

/** \brief Check if the char is an alphanumeric character
 *  \param c The char to check
 *  \return True if the char is an alphanumeric character
 */
bool isAlphaNum(const char &c)
{
    if (c < 127)
        return isalnum(c);
    else
        return true;
}

XMLNode::XMLNode() : mName(), mType(UNKNOWN), mClosed(false)
{

}

XMLNode::XMLNode(const std::string &name) : mName(name), mType(UNKNOWN), mClosed(false)
{

}

XMLNode::~XMLNode()
{
    while (!mChildren.empty())
    {
        delete *mChildren.begin();
        mChildren.erase(mChildren.begin());
    }
}

void XMLNode::setAttribute(const std::string &name, const std::string &value)
{
    mAttributes[name] = value;
}

std::string XMLNode::getAttribute(const std::string &name) const
{
    AttributesMap::const_iterator it = mAttributes.find(name);
    if (it != mAttributes.end())
        return it->second;
    else
        return std::string();
}

bool XMLNode::attributeExists(const std::string &name) const
{
    AttributesMap::const_iterator it = mAttributes.find(name);
    return it == mAttributes.end() ? false : true;
}

void XMLNode::removeChild(XMLNode *child)
{
    ChildrenVector::iterator it;
    for (it = mChildren.begin(); it != mChildren.end(); ++it)
    {
        if (*it == child)
        {
            delete child;
            mChildren.erase(it);
        }
    }
}

void XMLNode::removeChild(ushort index)
{
    if (index < mChildren.size())
    {
        delete mChildren[index];
        mChildren.erase(mChildren.begin() + index);
    }
}

XMLNode* XMLNode::findChild(const std::string &child) const
{
    for (const_iterator it = mChildren.begin(); it != mChildren.end(); ++it)
    {
        if ((*it)->getName() == child)
            return *it;
    }
    return 0;
}

bool XMLNode::parse(std::string &rest)
{
    if (rest.empty())
        return false;

/* Check if there are other things than xml tags */
    rest = stripTrailingWhiteSpace(rest);
    if (rest[0] != '<')                 // Everything other than an xml tag is incorrect
        throw XMLException("Incorrect char in file");

/* Get the end of the tag */
    std::string::size_type in = rest.find('>');
    if (in == std::string::npos)
        throw XMLException("Never ending tag");

/* Identify and parse the tag */
    std::string tag = rest.substr(0, in+1);
    mType = parseTag(tag);

/* If it's a comment remove it and return */
    if (!mType)
        return false;
    else if (mType != ELEMENT)
    {
        rest.erase(0,in+1);
        return true;
    }

    rest.erase(0,in+1);
    if (rest.empty() || mClosed)
        return true;

    while (!rest.empty() && !mClosed)
    {
    /* Search for children and the close tag */
        rest = stripTrailingWhiteSpace(rest);
        if (rest[0] != '<')     // We've got text in here
        {
            std::string text;
            ushort i = 0;
            while ( i < rest.size() && rest[i] != '<' && rest[i] != '>' )
                text += rest[i++];
            if (!text.empty())
            {
                setText(text);
                mType = TEXT;
            }
            rest.erase(0,i);
            continue;
        }

        if (rest[1] == '/')
        {
            std::string name;
            ushort i = 2;
            while (i < rest.size() && ( isAlphaNum(rest[i]) || rest[i] == '_' || rest[i] == '-' || rest[i] == ':' || rest[i] == '.') )
                name += rest[i++];
            if (name.empty())
            {
                throw XMLException("Empty close tag");
            }
            else if (name == mName)
            {
                mClosed = true;
                rest.erase(0,i+1);
                return true;
            }
        }

    /* Get the end of the tag */
        in = rest.find('>');
        if (in == std::string::npos)
            throw XMLException("Never ending tag");

        XMLNode *node = new XMLNode();
        if (!node->parse(rest))
        {
            delete node;
            return false;
        }
        addChild(node);
    }

    return true;
}

void XMLNode::printToFile(std::ostream &file, const ushort &col) const
{
    if (!file.good())
        return;
/* Start with the < and the name of the tag */
    std::string tabs;
    for (int i = 0; i < col; ++i)
        tabs += '\t';

    file << tabs << '<' << mName;

    for (AttributesMap::const_iterator it = mAttributes.begin(); it != mAttributes.end(); ++it)
        file << ' ' << it->first << "=\"" << it->second << '\"';

    if (mChildren.empty() && mText.empty())
    {
        file << " />\n";
        file.flush();
        return;
    }

    file << ">";

    if (!mText.empty())
    {
        file << mText;
        file << (mText[mText.size()-1] != '\n' ? "" : tabs) << "</" << mName << ">\n";
    }
    else
    {
        file << '\n';
        for (ChildrenVector::const_iterator it = mChildren.begin(); it != mChildren.end(); ++it)
            (*it)->printToFile(file, col+1);
        file << tabs << "</" << mName << ">\n";
    }

    file.flush();
}

XMLNode::Type XMLNode::identify(const std::string &tag) const
{
    if (tag.empty() || tag.size() < 2)
        return UNKNOWN;

/* Check the beginning */
    if (tag[0] != '<')
    {
        throw XMLException("Invalid char in file");
    }
    else if (tag.find("<!--") == 0)
    {
        if (tag.rfind("-->") == tag.size()-3)
            return COMMENT;
        else
            throw XMLException("Incorrect Comment");
    }
    else if (tag.find("<?xml") == 0)
    {
        return DEFINITION;
    }
    else if (tag.find("<!") == 0)
    {
        return DTD;
    }
    else if (isalpha(tag[1]) || tag[1] == '_' || tag[1] == '/')
    {
        return ELEMENT;
    }

    return UNKNOWN;
}

XMLNode::Type XMLNode::parseTag(const std::string &tag)
{
    if (tag.empty())
        return UNKNOWN;

/* Identify the tag */
    Type type = identify(tag);
    if (!type)
        return UNKNOWN;
    else if (type != ELEMENT)
        return type;

/* Get the name */
    ushort i = 1;
    std::string name;
    if (!isAlpha(tag[i]) && tag[i] != '_')  // XML tags may only begin with an alpha character or '_'
        throw XMLException("Incorrect char at the beginning of the tag");

    while (i < tag.size() && ( isAlphaNum(tag[i]) || tag[i] == '_' || tag[i] == '-' || tag[i] == ':' || tag[i] == '.') )
        name += tag[i++];

    if (!name.empty())
        setName(name);
    std::string rest = stripTrailingWhiteSpace(tag.substr(i));

    if (rest.empty())
    {
        return UNKNOWN;
    }
    else if (rest[0] == '/')
    {
        if (rest.size() == 2 && rest[1] == '>')
        {
            mClosed = true;
            return type;
        }
        else
        {
            throw XMLException("Incorrect usage of /");
        }
    }

/* There must be some attributes, find them */
    while (!rest.empty())
    {
    /* Check first if the tag ends here */
        if (rest[0] == '>')
        {
            return type;
        }
        else if (rest[0] == '/')
        {
            if (rest.size() == 2 && rest[1] == '>')
            {
                mClosed = true;
                return type;
            }
            else
            {
                throw XMLException("Incorrect usage of /");
            }
        }

        std::string value;
        name.clear();
        i = 0;
    /* Get the name of the attribute */
        if (!isAlpha(rest[i]) && rest[i] != '_')    // XML tags may only begin with an alpha character or '_'
            throw XMLException("Incorrect char at the beginning of the tag");

        while (i < rest.size() && ( isAlphaNum(rest[i]) || rest[i] == '_' || rest[i] == '-' || rest[i] == ':' || rest[i] == '.' ) )
            name += rest[i++];

        if (name.empty() || rest[i] != '=')
            throw XMLException("Incorrect Attribute in tag");

    /* Get the value */
        ++i;
        char delim = 0;
        if (rest[i] == '\"' || rest[i] == '\'')
            delim = rest[i];
        else
            throw XMLException("Missing delimiter in attribute");

        value = rest[i++];
        while (i < rest.size() && (rest[i] != delim) )
            value += rest[i++];
            
        if (i < rest.size() && rest[i] == delim)
            value += rest[i++];
        else if ( i == rest.size() )
            throw XMLException("Missing delimiter at the end of the attribute");
        
        setAttribute(name, stripClosingTags(value));
        rest = stripTrailingWhiteSpace(rest.substr(i));
    }

    return type;
}

/* Everything below needs to be rewritten */

XMLDocument::XMLDocument(const std::string &file) : mFile(file)
{

}

XMLDocument::XMLDocument() : mFile()
{

}

XMLDocument::~XMLDocument()
{

}

bool XMLDocument::parse()
{
    if (mFile.empty())
        return false;
    std::ifstream filesock;

    filesock.open(mFile.c_str(), std::ios::in);
    if (!filesock.is_open())
        return false;

    std::string file;
    while (!filesock.eof())
    {
        std::string line;
        getline(filesock, line);
        file += line + "\n";
    }
    file.erase(file.size()-1);
    filesock.close();
    
    return !file.empty() ? parse(file) : false;
}

bool XMLDocument::parse(const std::string &xml)
{
    if (xml.empty())
	return false;
    std::string file = stripTrailingWhiteSpace(xml);
    while ( file[0] == '<')
    {
        XMLNode *node = new XMLNode();
        if (!node->parse(file))
            return false;
        addChild(node);
        file = stripTrailingWhiteSpace(file);
    }
    if (!stripTrailingWhiteSpace(file).empty())
        throw XMLException("Invalid char at the end of file "+mFile);

    return true;
}

bool XMLDocument::save()
{
    std::ofstream filesock;

    filesock.open(mFile.c_str(), std::ios::out);
    if (!filesock.is_open())
        return false;

    bool ret = save(filesock);    
    filesock.close();
    return ret;
}

bool XMLDocument::save(std::ostream &out)
{
    if (!out.good())
        return false;

    for ( const_iterator it = begin(); it != end(); ++it)
        (*it)->printToFile(out, 0);

    out.flush();
    return true;
}
